Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(walletconnect): walletconnect integration #2223

Open
wants to merge 117 commits into
base: dev
Choose a base branch
from
Open

Conversation

borngraced
Copy link
Member

@borngraced borngraced commented Sep 16, 2024

This PR introduces the integration of WalletConnect into the Komodo DeFi Framework (KDF), enabling secure wallet connections for Cosmos and EVM-based chains. KDF acts as the DApp(in this PR), allowing users to initiate and manage transactions securely with external wallets e.g via Metamask.
Key changes include:

  • Implement multi-session handling for concurrent WalletConnect connections across accounts, integrates SQLite/IndexedDB for persistent session storage across devices, enhances relayer disconnection handling for smoother session management
  • Implemented WalletConnect coin activation for Tendermint and EVM.
  • Implemented Withdraw and Swap functionalities for Tendermint and EVM

https://specs.walletconnect.com/2.0/specs/clients/sign/
https://specs.walletconnect.com/2.0/specs/clients/core/pairing/
https://specs.walletconnect.com/2.0/specs/clients/core/crypto/
https://specs.walletconnect.com/2.0/specs/servers/relay/
https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc

Additional improvements include cleanup of unused dependencies, minor code refinements, and WASM compatibility

Updated deps:
Added deps:
Removed deps:

How to test using EVM coin e.g ETH (Native && WASM)

  1. Start kdf (cargo run)
  2. Create WalletConnect session connection
    • Use the RPC below
    • Copy the connection URL and either:
      • Paste directly in your wallet or
      • Generate QR code using QR Code Generator and scan with your wallet (e.g., MetaMask)
{
     "method": "wc_new_connection",
	"userpass": "{{ _.userpass }}",
	"mmrpc": "2.0",
	"params": {
		"namespaces": {
			"eip155": {
				"chains": ["eip155:1"]
				"methods": [
					"eth_sendTransaction",
					"eth_signTransaction",
					"personal_sign"
				],
				"events": [
					"accountsChanged",
					"chainChanged"
				]
			}
		}
	}
}
  1. Activate ETH coin (set "priv_key_policy": "WalletConnect", in activation params).
  2. Approve authentication request in your Wallet
  3. Withdraw or trade.

Note: To add more eip155 chains, modify the chains array like this: ["eip155:1", "eip155:250"]

@borngraced borngraced self-assigned this Sep 16, 2024
# Conflicts:
#	mm2src/coins/Cargo.toml
#	mm2src/coins_activation/src/eth_with_token_activation.rs
#	mm2src/mm2_core/src/mm_ctx.rs
@borngraced borngraced added under review and removed in progress Changes will be made from the author labels Nov 6, 2024
@shamardy
Copy link
Collaborator

shamardy commented Nov 8, 2024

@onur-ozkan This PR requires your review on the tendermint code changes at least

@shamardy shamardy added the P2 label Nov 11, 2024
@laruh
Copy link
Member

laruh commented Nov 12, 2024

@borngraced cargo lock conflict occurred

Comment on lines +52 to +53
/// Activation via WalletConnect
WalletConnect,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some info about why we don't take public key similar to how it's done with WithPubkey variant?

Also, I'm curious why WithPubkey mode doesn’t work here. Doesn’t WalletConnect already provide the public key via an API? WithPubkey mode was intended to support any type of external wallet as it only requires the public key and doesn’t depend on the wallet’s internal logic.

Copy link
Member Author

@borngraced borngraced Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WalletConnect doesn’t directly provide the public key as part of the initial session information. Instead, it only returns the account address, methods and the chains the user can interact with. To obtain the public key, a separate request is required for the specific account

https://specs.walletconnect.com/2.0/specs/clients/sign/rpc-methods#wc_sessionsettle
https://specs.walletconnect.com/2.0/specs/clients/sign/session-proposal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have done it in a way where some library/crate (or even GUI side) handles this and then utilizes with_pubkey on core KDF side to have single entrypoint for any external wallet, so the complexity remains constant for N external wallet types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is like dividing this implementation into two (manages connection from GUI while kdf manages request handling ) or leaving everything entirely to GUI which is not what we wanted from the beginning.

So the thing is whoever wishes to use any wallet pubkey via WalletConnect needs to make an additional request which can't really be avoided.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean was, we could handle that connection part and the extra request part (for getting pubkey) in a separate crate and utilize the with_pubkey mode. I mean, we have an abstraction layer for external wallets, so why not using it?

Copy link
Member Author

@borngraced borngraced Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could handle that connection part and the extra request part (for getting pubkey) in a separate crate

how do you plan to do this? since, we either ways we still need to request for pubkey meanwhile with_pubkey params requires pubkey in activation params itself. There's a kdf_walletconnect crate that handles all WC logic By the way,.

WalletConnect coin activation mode lets the activation logic handle the session and fetch the key directly, setting it in TendermintActivationPolicy::PublicKey—keeping things simple without needing an abstraction layer.

Also, the current external wallet implementation for tendermint uses SSE, in the case of Keplr, we need to send request to GUI then to Keplr wallet if I'm correct, so it can't directly work with WalletConnect. The current activation logic is quite easy to understand and scale(when integrating KDF as Wallet in the future)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you plan to do this? since, we either ways we still need to request for pubkey meanwhile with_pubkey params requires pubkey in activation params itself. There's a kdf_walletconnect crate that handles all WC logic By the way,.

User sends the activation request for WC -> KDF takes the request and runs the logic where it reads Pubkey from WC -> Uses that Pubkey to run KDF as an external wallet (just like any other wallet).

Is that not possible?

Also, the current external wallet implementation for tendermint uses SSE, in the case of Keplr, we need to send request to GUI then to Keplr wallet if I'm correct, so it can't directly work with WalletConnect. The current activation logic is quite easy to understand and scale(when integrating KDF as Wallet in the future)

Yeah, for Keplr, GUI side triggers the Keplr events which when required. But it doesn't have to be the same way for WalletConnect.

Copy link
Member Author

@borngraced borngraced Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User sends the activation request for WC -> KDF takes the request and runs the logic where it reads Pubkey from WC -> Uses that Pubkey to run KDF as an external wallet (just like any other wallet).

that's how the current flow works.. or am getting something wrong..
User request coin activation, logic checks if TendermintPubkeyActivationParams::WalletConnect( we can't use WithPubkey, since we don't have the pubkey at start)

TendermintPubkeyActivationParams::WalletConnect => {
activate_with_walletconnect(&ctx, protocol_conf.chain_id.as_ref(), &ticker).await?
},

then KDF takes the request and runs the logic where it reads Pubkey from WC( maybe this function should have a different naming)

async fn activate_with_walletconnect(
ctx: &MmArc,
chain_id: &str,
ticker: &str,
) -> MmResult<(TendermintActivationPolicy, TendermintWalletConnectionType), TendermintInitError> {
let wc = WalletConnectCtx::from_ctx(ctx).expect("WalletConnectCtx should be initialized by now!");
let account = cosmos_get_accounts_impl(&wc, chain_id)
.await
.mm_err(|err| TendermintInitError {
ticker: ticker.to_string(),
kind: TendermintInitErrorKind::UnableToFetchChainAccount(err.to_string()),
})?;
let wallet_type = if wc.is_ledger_connection().await {
TendermintWalletConnectionType::WcLedger
} else {
TendermintWalletConnectionType::Wc
};
let pubkey = match account.algo {
CosmosAccountAlgo::Secp256k1 | CosmosAccountAlgo::TendermintSecp256k1 => {
TendermintPublicKey::from_raw_secp256k1(&account.pubkey).ok_or(TendermintInitError {
ticker: ticker.to_string(),
kind: TendermintInitErrorKind::Internal("Invalid secp256k1 pubkey".to_owned()),
})?
},
};
Ok((TendermintActivationPolicy::with_public_key(pubkey), wallet_type))
}

Uses that Pubkey to run KDF as an external wallet (just like any other wallet).

Ok((TendermintActivationPolicy::with_public_key(pubkey), wallet_type))

mm2src/kdf_walletconnect/src/chain.rs Show resolved Hide resolved
mm2src/coins/tendermint/tendermint_token.rs Show resolved Hide resolved
Copy link
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Very organized and easy to review.
Here is the first iteration.

@@ -2,7 +2,7 @@
JEMALLOC_SYS_WITH_MALLOC_CONF = "background_thread:true,narenas:1,tcache:false,dirty_decay_ms:0,muzzy_decay_ms:0,metadata_thp:auto"

[target.'cfg(all())']
rustflags = [ "-Zshare-generics=y" ]
rustflags = [ "-Zshare-generics=y", '--cfg=curve25519_dalek_backend="fiat"' ]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: what is that for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selecting the backend for compiling curve25519-dalek. This is explicitly needed because I updated the lib which now supports different backends for compilation

https://crates.io/crates/curve25519-dalek/4.1.3#simd-backend

Comment on lines +314 to +315
// #[serde(default)]
// pub use_walletconnect: Option<bool>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cruft

let public_key = compress_public_key(public_key_uncompressed)?;
Ok((
EthPrivKeyPolicy::WalletConnect {
address,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this address doesn't seem to be used anywhere

.send_session_request_and_wait(&chain_id, WcRequestMethods::EthSignTransaction, tx_json, Ok)
.await?;
// First 4 bytes from WalletConnect represents Protoc info
hex::decode(&tx_hex[4..])?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the same as .strip_prefix(0x) in wc_send_tx? if so let's use the same for both.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop, this is some other WalletConnect protoc related data which is 4 bytes. stripprefix strips first 2 bytes I think

let tx_hash = tx_hash.strip_prefix("0x").unwrap_or(&tx_hash);
let maybe_signed_tx = {
// Wait for 10 seconds for the transaction to appear on the RPC node.
let wait_rpc_timeout = 10_000;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should either set this as a const for visibility or mention in the doc comment that it waits for x amount of seconds.
also, the check_every seems very quick (1ms?)

Comment on lines +229 to +238
#[cfg(not(target_arch = "wasm32"))]
native_only_methods => match native_only_methods {
#[cfg(all(feature = "enable-solana", not(target_os = "ios"), not(target_os = "android")))]
"enable_solana_with_tokens" => {
handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::<SolanaCoin>).await
},
#[cfg(all(feature = "enable-solana", not(target_os = "ios"), not(target_os = "android")))]
"enable_spl" => handle_mmrpc(ctx, request, enable_token::<SplToken>).await,
_ => MmError::err(DispatcherError::NoSuchMethod),
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cruft from a mis-merge

Ok((uncompressed, recovered_address))
}

pub(crate) fn recover(signature: &Signature, message: &Message) -> Result<H520, ethkey::Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use the Public type alias here for the Ok return type. one is not sure at first glance what this function recovers.

@@ -4744,6 +4748,7 @@ pub fn derive_htlc_key_pair(coin: &UtxoCoinFields, _swap_unique_data: &[u8]) ->
PrivKeyPolicy::Trezor => todo!(),
#[cfg(target_arch = "wasm32")]
PrivKeyPolicy::Metamask(_) => panic!("`PrivKeyPolicy::Metamask` is not supported for UTXO coins"),
PrivKeyPolicy::WalletConnect { .. } => todo!(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more descriptive panic would help here.
nit as of now: we might wanna refactor this method to not panic.

Comment on lines +64 to +65
[patch.crates-io]
rand_core = { version = "0.6.2", package = "bip39" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: can't understand this patch. why doesn't it have a url/git? could you explain how/what this does.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm forcing bip39 to depend on rand_core 0.6.2 since the lib is selecting the least supported version(0.4.2) in kdf even tho the version isn't really compatible with the latest version of bip39
https://crates.io/crates/bip39/2.1.0/dependencies, I mean the feature of bip39 we using in kdf requires rand_core 0.6.2 or greater

The lib is configured to select any version from 0.4.2==0.7

@@ -48,8 +48,9 @@ mod dispatcher_legacy;
#[path = "rpc/lp_commands/lp_commands_legacy.rs"]
pub mod lp_commands_legacy;
mod rate_limiter;
#[path = "rpc/wc_commands/wc_commands.rs"] pub mod wc_commands;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename wc_commands.rs -> mod.rs and not use the path attribute here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants